using System;
using System.Collections.Generic;
using System.Linq;

using System.Reflection;

using FluentNHibernate.Conventions.Instances;
using FluentNHibernate.Mapping;
using FluentNHibernate.Mapping.Providers;

/*
 This robust collection mapper doesn't work well when the POCO is separate from mapper's assembly
 
 Can only be used if POCO and mapper are in same assembly
 
 
 */

namespace FluentNHibernate.RobustCollectionMapper
{
	
   public class ClassMapExt<T> : ClassMap<T>
    {
        public IList<IManyToOneMappingProvider> ExtReferences { get { return this.references; } }
        public IList<ICollectionMappingProvider> ExtCollections { get { return this.collections; } }
    }
	
	public class RobustReferenceMappingException : Exception
	{
		public RobustReferenceMappingException() {}
		
		public RobustReferenceMappingException(string message) : base(message) {}
		
		public RobustReferenceMappingException(string message, Exception inner) : base(message,inner) {}
	}

	
	
	
	public static class Helper
	{
		
		
		        /*
         * 
         * This performs an extra step to make sure that the ClassMap's HasMany corresponds well with ClassMap's References
         * 
         * Also checks if collection mapping and child references doesn't have ambiguity. This does this by checking if there are 
         * already two or more collections(HasMany); if it is, NormalizeReference will throw an Exception prompting the dev 
         * to explicitly name the joining column name on both ends of the collection and its References. And also, if the given
         * name on collection's KeyColumn doesn't match the References, this will also throw an Exception prompting the dev to make 
         * the Column's name on References match its referenced collection's KeyColumn's name
         * 
         */
 
		public static string NormalizeReference(this IOneToManyCollectionInstance x)
        {
            
			
 			
             
            string cannotDeduceReferenceFromValue = "Ambiguous references found. Do explicit column mapping on both end of the objects";
            string cannotDeduceCollectionFromEntity = "Ambiguous collection found. Do explicit column mapping on both end of the objects";
 
            string parentKeyfield = "";
 
            bool needExplicitness = false;
            // Find ambiguous in parent
            {
                
 
                var parentType = x.EntityType; // e.g. Person
 
                // Find ClassMapExt of the parentType(e.g. Person)
                var parent = (from r in x.ChildType.Assembly.GetTypes()
                              where r.BaseType.IsGenericType
                                    && r.BaseType.GetGenericTypeDefinition() == typeof(ClassMapExt<>)
                                    && r.BaseType.GetGenericArguments()[0] == parentType
                              select r).Single(); // there is only one class mapping for any objects.
 
                var parentInstance = Activator.CreateInstance(parent);
                var parentCollectionsOfChildType = 
                            from cr in ((IList<ICollectionMappingProvider>)parent.InvokeMember("ExtCollections", BindingFlags.GetProperty, null, parentInstance, null))
                            where cr.GetCollectionMapping().ChildType == x.ChildType
                            select cr;
 
 
                if (parentCollectionsOfChildType.Count() == 1)
                    parentKeyfield = parentCollectionsOfChildType.Single().GetCollectionMapping().Key.Columns.Single().Name;
                else
                {
                    // example: Contacts.  must match one parentCollectionsOfChildType only
                    parentKeyfield = parentCollectionsOfChildType.Where(y => y.GetCollectionMapping().Member.Name == x.Member.Name)
                                    .Single().GetCollectionMapping().Key.Columns.Single().Name;
                }
 
  
                bool hasAmbigousCollection =
                        parentCollectionsOfChildType.Count() > 1
                        &&
                        parentCollectionsOfChildType.Any(z => !z.GetCollectionMapping().Key.Columns.HasUserDefined());
 
 
 
 
                if (hasAmbigousCollection)
                    throw new RobustReferenceMappingException(
					         string.Format("{0}\nChild: {1} Parent: {2}",
					              cannotDeduceCollectionFromEntity, 
					              x.ChildType.Name, parentType.Name));
 
                needExplicitness = parentCollectionsOfChildType.Any(z => z.GetCollectionMapping().Key.Columns.HasUserDefined());
            }
 
             
 
            // Find ambiguous in children
            {
 
                // Find ClassMapExt of the x.ChildType(e.g. Contact)
                var child = (from r in x.ChildType.Assembly.GetTypes()
                             where r.BaseType.IsGenericType
                                   && r.BaseType.GetGenericTypeDefinition() == typeof(ClassMapExt<>)
                                   && r.BaseType.GetGenericArguments()[0] == x.ChildType  // Contact
                             select r).Single();
 
 
 
                 
 
                var childInstance = Activator.CreateInstance(child); // ContactMapExt                                        
 
                 
 
                /*
                 * 
                 * References(x => x.Owner)
                 * the Owner's property type is: Person                
                 * can be obtained from:
                 *      cr.GetManyToOneMapping().Member.PropertyType 
                 * 
                 * x.EntityType is: Person
                 * 
                 * */
 
                var childReferences =
                                    from cr in ((IList<IManyToOneMappingProvider>)child.InvokeMember("ExtReferences", BindingFlags.GetProperty, null, childInstance, null))
                                    where cr.GetManyToOneMapping().Member.PropertyType == x.EntityType
                                    select cr;
                                       
 
              
 
                /*
                 if you do in Classmap: References(x => x.Owner).Column("Apple")
                   
                        y.GetManyToOneMapping().Columns.Single().Name == "Apple"
                        
                        and y.GetManyToOneMapping().Columns.HasUserDefined() == true
                  
                 if you do in Classmap: References(x => x.Owner)
                   
                        y.GetManyToOneMapping().Columns.Single().Name == "Owner_id"
                        
                        and y.GetManyToOneMapping().Columns.HasUserDefined() == false
                  
                 in both cases:
                     
                        y.GetManyToOneMapping().Name == "Owner"
                        
                
                
                 If there's NO ambiguity(i.e. no multiple collections on an Entity), e.g. ...
                 
                    public class Person
                    {
                        public virtual int Person_Id { get; set; }
                        
                        public virtual string PersonName { get; set; }
                        public virtual DateTime Birthdate { get; set; }     
                                    
                        public virtual IList<Contact> Contacts { get; set; }
                    }
                    
                    public class Contact
                    {
                        public virtual Person Owner { get; set; }    
                  
                        public virtual int Id { get; set; }     
                
                        public virtual string Number { get; set; }      
                        public virtual string Type { get; set; }                    
                    }
                    
                 ..., NormalizeReference will automatically assign IList Contacts' KeyColumn name with
                 entity's child's referencing property variable name, 
                 i.e. variable name of property + referenceSuffix, e.g. "Owner_id". 
                 
                 If one uses vanilla ClassMap (i.e. sans NormalizeReference), the Person Owner will automatically result to :
                 
                       Reference' property's entity type + "_id" (default suffix gleaned from Fluent NHibernate source code); 
                       
                 in the example here, referencing property(public Person Owner)'s entity type is Person, then that will automatically
                 result to a KeyColumn name of Person_id on IList Contacts.  Which is not always desirable, if you have a naming 
                 pattern(could be from brownfield systems too) like this (note owner_id, it's not person_id)...
                
                     create table person (person_id int4 not null, person_name text not null, birthdate timestamp not null, 
                                    primary key (person_id));               
                       
                     create table contact (contact_id int4 not null, number text, type text, 
                          owner_id int4, primary key (contact_id));
                          
                
                    alter table contact 
                          add constraint fk_contact__person
                          foreign key (owner_id) references person(person_id);
                
                ...,a vanilla ClassMap will produce this:

                    create table person (person_id int4 not null, person_name text not null, birthdate timestamp not null, 
                                    primary key (person_id));
                
                    create table contact (contact_id int4 not null, 
                      number text, type text,                     
                      Owner_id int4, 
                      Person_id int4,                     
                      primary key (contact_id));
                      
                    alter table contact 
                          add constraint FK38B7242018FA494F 
                          foreign key (Owner_id) references person;
                          
                    alter table contact 
                          add constraint FK38B724202B329A0D 
                          foreign key (Person_id) references person;          
                
                
                    Note the double creation of references on child table (Owner_id and Person_id), even we only have one reference 
                    from child table (public virtual Person Owner).
                    
                    
                    That is the kind of thing that NormalizeReferences also solved, 
                    NormalizeReferences will resolve the Person's Contacts' KeyColumn to "Owner_id" only.

                                        
                    Problem also documented here: http://stackoverflow.com/questions/4437655/whats-the-right-fluent-nhibernate-conventionbuilder-for-differing-name-on-child                                    
                 */
 
 
 
 
                if (needExplicitness)
                {
                    // all not defined
                    if (childReferences.All(y => !y.GetManyToOneMapping().Columns.HasUserDefined()))
                    {
                        throw new RobustReferenceMappingException(
                            string.Format("Explicitness needed on both ends. {0}'s {1} has no corresponding explicit Reference to {2}",
                            x.EntityType.Name, x.Member.Name, x.ChildType.Name));
                    }// all not defined
                    else
                    {
                        var isParentKeyExistingInChildObject = childReferences.Any(z => z.GetManyToOneMapping().Columns.Single().Name == parentKeyfield);
 
                        if (!isParentKeyExistingInChildObject)
                        {
                            if (childReferences.Count() == 1)
                            {
                                string userDefinedKey = childReferences.Single().GetManyToOneMapping().Columns.Single().Name;
                                throw new RobustReferenceMappingException(
                                        string.Format(
                                            "Child object {0} doesn't match its key name to parent object {1}'s {2}. Child Key: {3} Parent Key: {4}",
                                            x.ChildType.Name, x.EntityType.Name, x.Member.Name, userDefinedKey, parentKeyfield)
                                            );
                            }
                            else
                            {
                                throw new RobustReferenceMappingException(
                                        string.Format(
                                            "Children object {0} doesn't match any key to parent object {1}'s {2}. Parent Key: {3}",
                                             x.ChildType.Name, x.EntityType.Name, x.Member.Name, parentKeyfield));
                            }
                        }//if
                        else
                        {
                            return parentKeyfield;
                        }
                    }//if at least one defined
                }// if needExplicitness
                else
                {
                    bool hasUserDefined = childReferences.Count() == 1 && childReferences.Any(y => y.GetManyToOneMapping().Columns.HasUserDefined());
 
                    if (hasUserDefined)
                    {
                        throw new RobustReferenceMappingException(
                                string.Format("Child object {0} has explicit Reference while the parent object {1} has none. Do explicit column mapping on both ends",
                                            x.ChildType.Name, x.EntityType.Name));
                    }
                }
 
                bool hasAmbiguousReference =
                    ( childReferences.Count() > 1 && childReferences.Any(y => !y.GetManyToOneMapping().Columns.HasUserDefined()) )
                     
                     
                    ||
                     
                     
                    ( !needExplicitness && childReferences.Any(y => y.GetManyToOneMapping().Columns.HasUserDefined()) );
 
 
                if (hasAmbiguousReference)
                    throw new RobustReferenceMappingException(
					           string.Format("{0}\nChild object: {1} Parent object: {2}",
					                        cannotDeduceReferenceFromValue,
					                        x.ChildType.Name, x.EntityType.Name));
 
 
                return childReferences.Single().GetManyToOneMapping().Name;
            }
 
 
            
 
        }//Normalize
	}
}

